﻿// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// ReSharper disable CheckNamespace
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
// ReSharper disable UnusedParameter.Local

/* LineBreaker.cs --
 * Ars Magna project, http://arsmagna.ru
 */

#region Using directives

using System.Collections.Generic;

using AM.Skia.RichTextKit.Utils;

#endregion

#nullable enable

namespace AM.Skia.RichTextKit;

/// <summary>
/// Implementation of the Unicode Line Break Algorithm
/// </summary>
internal class LineBreaker
{
    /// <summary>
    /// Reset this line breaker
    /// </summary>
    /// <param name="str">The string to be broken</param>
    public void Reset (string str)
    {
        Reset (new Slice<int> (Utf32Utils.ToUtf32 (str)));
    }

    /// <summary>
    /// Reset this line breaker
    /// </summary>
    /// <param name="codePoints">The code points of the string to be broken</param>
    public void Reset (Slice<int> codePoints)
    {
        _codePoints = codePoints;
        _first = true;
        _pos = 0;
        _lastPos = 0;
        _LB8a = false;
        _LB21a = false;
        _LB30a = 0;
    }

    private Slice<int> _codePoints;
    private bool _first = true;
    private int _pos;
    private int _lastPos;
    private LineBreakClass _curClass;
    private LineBreakClass _nextClass;
    private bool _LB8a;
    private bool _LB21a;
    private int _LB30a;

    /// <summary>
    /// Enumerate all line breaks
    /// </summary>
    /// <returns>A collection of line break positions</returns>
    public List<LineBreak> GetBreaks (bool mandatoryOnly = false)
    {
        var list = new List<LineBreak>();
        if (mandatoryOnly)
        {
            list.AddRange (FindMandatoryBreaks());
        }
        else
        {
            while (NextBreak (out var lb))
                list.Add (lb);
        }

        return list;
    }

    private LineBreakClass mapClass (LineBreakClass c)
    {
        switch (c)
        {
            case LineBreakClass.AI:
                return LineBreakClass.AL;

            case LineBreakClass.SA:
            case LineBreakClass.SG:
            case LineBreakClass.XX:
                return LineBreakClass.AL;

            case LineBreakClass.CJ:
                return LineBreakClass.NS;

            default:
                return c;
        }
    }

    private LineBreakClass mapFirst (LineBreakClass c)
    {
        switch (c)
        {
            case LineBreakClass.LF:
            case LineBreakClass.NL:
                return LineBreakClass.BK;

            case LineBreakClass.SP:
                return LineBreakClass.WJ;

            default:
                return c;
        }
    }

    // Get the next character class
    private LineBreakClass nextCharClass()
    {
        return mapClass (UnicodeClasses.LineBreakClass (_codePoints[_pos++]));
    }

    private bool? getSimpleBreak()
    {
        // handle classes not handled by the pair table
        switch (_nextClass)
        {
            case LineBreakClass.SP:
                return false;

            case LineBreakClass.BK:
            case LineBreakClass.LF:
            case LineBreakClass.NL:
                _curClass = LineBreakClass.BK;
                return false;

            case LineBreakClass.CR:
                _curClass = LineBreakClass.CR;
                return false;
        }

        return null;
    }

    private bool getPairTableBreak (LineBreakClass lastClass)
    {
        // if not handled already, use the pair table
        var shouldBreak = false;
        switch (LineBreakPairTable.table[(int)_curClass][(int)_nextClass])
        {
            case LineBreakPairTable.DI_BRK: // Direct break
                shouldBreak = true;
                break;

            case LineBreakPairTable.IN_BRK: // possible indirect break
                shouldBreak = lastClass == LineBreakClass.SP;
                break;

            case LineBreakPairTable.CI_BRK:
                shouldBreak = lastClass == LineBreakClass.SP;
                if (!shouldBreak)
                {
                    shouldBreak = false;
                    return shouldBreak;
                }

                break;

            case LineBreakPairTable.CP_BRK: // prohibited for combining marks
                if (lastClass != LineBreakClass.SP)
                {
                    return shouldBreak;
                }

                break;

            case LineBreakPairTable.PR_BRK:
                break;
        }

        if (_LB8a)
        {
            shouldBreak = false;
        }

        // Rule LB21a
        if (_LB21a && _curClass is LineBreakClass.HY or LineBreakClass.BA)
        {
            shouldBreak = false;
            _LB21a = false;
        }
        else
        {
            _LB21a = (_curClass == LineBreakClass.HL);
        }

        // Rule LB30a
        if (_curClass == LineBreakClass.RI)
        {
            _LB30a++;
            if (_LB30a == 2 && (_nextClass == LineBreakClass.RI))
            {
                shouldBreak = true;
                _LB30a = 0;
            }
        }
        else
        {
            _LB30a = 0;
        }

        _curClass = _nextClass;

        return shouldBreak;
    }


    public bool NextBreak (out LineBreak lineBreak)
    {
        // get the first char if we're at the beginning of the string
        if (_first)
        {
            _first = false;
            var firstClass = nextCharClass();
            _curClass = mapFirst (firstClass);
            _nextClass = firstClass;
            _LB8a = (firstClass == LineBreakClass.ZWJ);
            _LB30a = 0;
        }

        while (_pos < _codePoints.Length)
        {
            _lastPos = _pos;
            var lastClass = _nextClass;
            _nextClass = nextCharClass();

            // explicit newline
            if ((_curClass == LineBreakClass.BK) ||
                ((_curClass == LineBreakClass.CR) && (_nextClass != LineBreakClass.LF)))
            {
                _curClass = mapFirst (mapClass (_nextClass));
                lineBreak = new LineBreak (findPriorNonWhitespace (_lastPos), _lastPos, true);
                return true;
            }

            var shouldBreak = getSimpleBreak();

            if (!shouldBreak.HasValue)
            {
                shouldBreak = getPairTableBreak (lastClass);
            }

            // Rule LB8a
            _LB8a = (_nextClass == LineBreakClass.ZWJ);

            if (shouldBreak.Value)
            {
                lineBreak = new LineBreak (findPriorNonWhitespace (_lastPos), _lastPos, required: false);
                return true;
            }
        }

        if (_lastPos < _codePoints.Length)
        {
            _lastPos = _codePoints.Length;
            var required = (_curClass == LineBreakClass.BK) ||
                           ((_curClass == LineBreakClass.CR) && (_nextClass != LineBreakClass.LF));
            lineBreak = new LineBreak (findPriorNonWhitespace (_codePoints.Length), _lastPos, required);
            return true;
        }
        else
        {
            lineBreak = new LineBreak (0, 0, required: false);
            return false;
        }
    }

    public IEnumerable<LineBreak> FindMandatoryBreaks()
    {
        for (var i = 0; i < _codePoints.Length; i++)
        {
            var cls = UnicodeClasses.LineBreakClass (_codePoints[i]);
            switch (cls)
            {
                case LineBreakClass.BK:
                    yield return new LineBreak (i, i + 1, true);
                    break;

                case LineBreakClass.CR:
                    if (i + 1 < _codePoints.Length &&
                        UnicodeClasses.LineBreakClass (_codePoints[i + 1]) == LineBreakClass.LF)
                    {
                        yield return new LineBreak (i, i + 2, true);
                    }
                    else
                    {
                        yield return new LineBreak (i, i + 1, true);
                    }

                    break;

                case LineBreakClass.LF:
                    yield return new LineBreak (i, i + 1, true);
                    break;
            }
        }
    }

    private int findPriorNonWhitespace (int from)
    {
        if (from > 0)
        {
            var cls = UnicodeClasses.LineBreakClass (_codePoints[from - 1]);
            if (cls is LineBreakClass.BK or LineBreakClass.LF or LineBreakClass.CR)
            {
                from--;
            }
        }

        while (from > 0)
        {
            var cls = UnicodeClasses.LineBreakClass (_codePoints[from - 1]);
            if (cls == LineBreakClass.SP)
            {
                from--;
            }
            else
            {
                break;
            }
        }

        return from;
    }
}
